﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.ComponentModel.Design;
using System.Windows;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Mono.Cecil;
using NDasm.Addin.UI;
using NDasm.Core;

namespace NDasm.Addin
{    
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.2", IconResourceID = 400)]
    [ProvideMenuResource("Menus.ctmenu", 1)]
    [ProvideToolWindow(typeof(NDasmWindow), MultiInstances = true, Style = VsDockStyle.MDI, Transient = true)]
    [Guid(GuidList.guidNDasmPkgString)]
    [ProvideAutoLoad("{f1536ef8-92ec-443c-9ed7-fdadf150da82}")]
    public sealed class NDasmPackage : Package
    {
        private DTE dte;
        private OleMenuCommand menuItem;
        private int nextWindowId = 0;
        private List<NDasmWindow> windows = new List<NDasmWindow>();

        protected override void Initialize()
        {
            base.Initialize();
            dte = (DTE)GetService(typeof(DTE));
            OleMenuCommandService menuService = (OleMenuCommandService)GetService(typeof(IMenuCommandService));            
            CommandID commandId = new CommandID(GuidList.guidNDasmCmdSet, (int)PkgCmdIDList.cmdidNDasm);
            menuItem = new OleMenuCommand(NDasmCommandCallback, commandId);
            menuItem.BeforeQueryStatus += NDasmCommandStatusCallback;
            menuService.AddCommand(menuItem);
        }

        private void NDasmCommandStatusCallback(object sender, EventArgs e)
        {
            menuItem.Visible = GetCodeElementFromCursor() != null;
        }

        private void NDasmCommandCallback(object sender, EventArgs e)
        {
            var codeElement = GetCodeElementFromCursor();
            if (codeElement == null)
                return;
            var project = codeElement.ProjectItem.ContainingProject;
            dte.Solution.SolutionBuild.BuildProject(project.ConfigurationManager.ActiveConfiguration.ConfigurationName,
                                                    project.UniqueName, true);
            if (dte.Solution.SolutionBuild.LastBuildInfo != 0)
                return;
            var path = project.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value.ToString();
            var fullPath = Helper.GetFullPath(path, Path.GetDirectoryName(project.FileName));
            var assemblyPath = Path.Combine(fullPath, project.Properties.Item("OutputFileName").Value.ToString());
            DisplayDasm(assemblyPath, codeElement);
        }

        private void DisplayDasm(string assemblyPath, CodeElement2 codeElement)
        {
            ModuleDefinition module = ModuleDefinition.ReadModule(assemblyPath);
            switch (codeElement.Kind)
            {
                case vsCMElement.vsCMElementNamespace:
                    DisplayAssemblyDasm(module);
                    break;                
                case vsCMElement.vsCMElementClass:
                    DisplayTypeDasm(module, codeElement.FullName);
                    break;
                case vsCMElement.vsCMElementFunction:
                    DisplayMethodDasm(module, codeElement.FullName);
                    break;                             
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private void DisplayTypeDasm(ModuleDefinition module, string typeName)
        {
            TypeDefinition type = module.GetType(typeName);
            if (type == null)
            {
                const string message = "Type {0} not found in assembly {1}.\nCan't display disassembly.";
                MessageBox.Show(String.Format(message, typeName, module.Assembly.Name), "NDasm", MessageBoxButton.OK,
                                MessageBoxImage.Error);
                return;
            }
            NDasmWindow window = GetWindow(module);
            window.Initialize(module, type);
            window.Show();     
        }

        private void DisplayAssemblyDasm(ModuleDefinition module)
        {
            NDasmWindow window = GetWindow(module);
            window.Initialize(module);
            window.Show();
        }

        private void DisplayMethodDasm(ModuleDefinition module, string methodName)
        {
            MethodDefinition method = CoreHelper.GetMethod(module, methodName);            
            if (method == null)
            {
                const string message = "Method {0} not found in assembly {1}.\nCan't display disassembly.";
                MessageBox.Show(String.Format(message, methodName, module.Assembly.Name), "NDasm", MessageBoxButton.OK,
                                MessageBoxImage.Error);
                return;
            }
            NDasmWindow window = GetWindow(module);                    
            window.Initialize(module, method);
            window.Show();            
        }

        private NDasmWindow GetWindow(ModuleDefinition module)
        {
            var window = windows.Find(w => w.AssemblyPath == module.FullyQualifiedName);
            if (window == null)
            {
                window = (NDasmWindow)FindToolWindow(typeof(NDasmWindow), nextWindowId, true);
                window.Closed += w => windows.Remove(w);
                windows.Add(window);
                nextWindowId++;
            }
            return window;
        }

        private CodeElement2 GetCodeElementFromCursor()
        {
            CodeElement2 codeElement = GetSpecificCodeElementFromCursor(vsCMElement.vsCMElementFunction);
            if (codeElement == null)
                codeElement = GetSpecificCodeElementFromCursor(vsCMElement.vsCMElementClass);
            if (codeElement == null)
                codeElement = GetSpecificCodeElementFromCursor(vsCMElement.vsCMElementNamespace);
            return codeElement;
        }

        private CodeElement2 GetSpecificCodeElementFromCursor(vsCMElement elementKind)
        {
            if (dte.ActiveDocument == null)
                return null;
            var codeModel = dte.ActiveDocument.ProjectItem.FileCodeModel;
            if (codeModel == null)
                return null;
            dynamic selection = dte.ActiveDocument.Selection;
            var point = selection.ActivePoint;
            try
            {
                // could throw if cursor not in the specified elementKind
                return point != null ? codeModel.CodeElementFromPoint(point, elementKind) : null;
            }
            catch (Exception)
            {
                return null;
            }            
        }
    }
}
